#include "GL/GLGraphicsDevice.h"
#include "GL/GLVertexBuffer.h"
#include "GL/GLIndexBuffer.h"
#include "GL/GLRenderTexture.h"
#include "GL/GLIndexBuffer.h"
#include "GL/GLVertexBuffer.h"
#include "GL/GLContext.h"
#include "RenderOperation.h"
#include "Exception.h"
#include "Vertex.h"
#include "IResourceManager.h"
#include "Logger.h"
#include "IContext.h"

#define BUFFER_OFFSET(i) ((char *)NULL + (i))

NS_B2D_BEGIN

GLGraphicsDevice::GLGraphicsDevice(void)
	: mActiveAttributes(0)
{
	GLContext::InitialiseShared();

	IResourceManager& rm = *GetContext().getInstance<IResourceManager>();
	rm.registerFactory( mTextureFactory );
	rm.registerFactory( mShaderProgramFactory );
}

GLGraphicsDevice::~GLGraphicsDevice(void)
{
	GLContext::DestroyShared();
}
	
void GLGraphicsDevice::setViewport(int x, int y, size_t w, size_t h)
{
	glViewport((GLint)x, (GLint)y, (GLsizei)w, (GLsizei)h);
}

RenderTexture* GLGraphicsDevice::createRenderTexture(size_t texCount)
{
	return new GLRenderTexture(texCount);
}

VertexBuffer* GLGraphicsDevice::createVertexBuffer(void)
{
	return new GLVertexBuffer();
}

IndexBuffer* GLGraphicsDevice::createIndexBuffer(void)
{
	return new GLIndexBuffer();
}


IGraphicContext* GLGraphicsDevice::_createGraphicsContext(void)
{
	return new GLContext();
}

void GLGraphicsDevice::_setActiveRenderTarget(const RenderTarget& rt)
{
	if( getActiveRenderTarget() != &rt )
	{
		GraphicsDevice::_setActiveRenderTarget(rt);
		GLuint fbo = reinterpret_cast<GLuint>(mActiveRenderTarget->getRTHandle());
	}
}

void GLGraphicsDevice::setActiveShaderProgramImpl(void)
{
	GLuint sp = (mActiveShaderProgram == nullptr) ? 0 : std::static_pointer_cast<GLShaderProgram>(mActiveShaderProgram)->getHandle();
	glUseProgram(sp);
	B2D_LOG_DEBUG("Active shader set to \"%s\" (id=%i).\n",
		(mActiveShaderProgram!=nullptr) ? mActiveShaderProgram->getName().getString().c_str():  "",
		sp);
}

void GLGraphicsDevice::setTextureSlot(TextureUnit::Unit slot, TexturePtr texture)
{
	mTextureSlots[slot] = texture;
	glActiveTexture(GL_TEXTURE0 + slot);
	GLuint texHandle = (texture.get() == nullptr) ? 0 : std::static_pointer_cast<GLTexture>(texture)->getGLName();
	glBindTexture(GL_TEXTURE_2D, texHandle);
}

void GLGraphicsDevice::setBlendingImpl(void)
{
	if(mBlendSrcCoeff == BlendCoefficient::ONE && mBlendDstCoeff == BlendCoefficient::ZERO)
	{
		glDisable(GL_BLEND);
	}
	else
	{
		GLenum glSrc = GLGraphicEnumConverter::getBlendingCoefficient(mBlendSrcCoeff);
		GLenum glDst = GLGraphicEnumConverter::getBlendingCoefficient(mBlendDstCoeff);
		glEnable(GL_BLEND);
		glBlendFunc(glSrc, glDst);
	}

	GLenum glFunc = GLGraphicEnumConverter::getBlendingFunction(mBlendFunction);
	glBlendEquation(glFunc);
}

void GLGraphicsDevice::_render(const RenderOperation& op)
{
	// Make sure at least 1 vertex buffer
	size_t vbo_count = op.vertexBuffers.size();
	if( vbo_count < 1 )
	{
		B2D_THROW(Exception::INVALID_ARGUMENT_EXCEPTION,
			"Can not draw geometry without any vertex buffers");
	}

	// Enable vertex buffers & bind attributes
	// Also enable vertex attributes that are disabled but needed
	VertexFormat attRequested;
	for(size_t i = 0; i < vbo_count; ++i)
	{
		// Bind vbo
		VertexBuffer* vbo = op.vertexBuffers[i];
		GLVertexBuffer::BindLock tmpBind( reinterpret_cast<GLuint>(vbo->getHandle()) );

		VertexFormat vbo_format = vbo->getVertexFormat();
		size_t offset = 0;
		for(size_t j = 0; j < VertexAttribute::COUNT; ++j)
		{
			size_t attFlag = 1 << j;
			if( attFlag & vbo_format )
			{
				// Add flag
				attRequested |= attFlag;

				// Enable attribute if required
				if( (attFlag & mActiveAttributes) == 0 )
				{
					glEnableVertexAttribArray(j);
					mActiveAttributes |= attFlag;
				}
				// Set vertex pointer
				size_t attSize = VertexAttribute::sizes[j];
				size_t attElems = VertexAttribute::elements[j];
				glVertexAttribPointer(j, attElems, GLVertexAttribute::dataTypes[j], false, vbo->getVertexSize(), BUFFER_OFFSET(offset));
				offset += attSize;
			}
		}
	}

	// Disable attributes not required but enabled
	for(size_t i = 0; i < VertexAttribute::COUNT; ++i)
	{
		VertexFormat mask = 1 << i;
		if( (mActiveAttributes & mask) && !(attRequested & mask) )
		{
			glDisableVertexAttribArray(i);
		}
	}
	mActiveAttributes = attRequested;

	// Get draw call settings
	GLsizei count = (GLsizei)op.vertexCount;
	GLint first = (GLint)op.vertexOffset;
	GLenum primitive = GLGraphicEnumConverter::getPrimitiveType( op.primitive );

	
	// Choose draw call according to index buffer
	if( op.indexBuffer != nullptr || op.unbufferedIndexData != nullptr )
	{
		const GLvoid* indices = nullptr;
		GLenum index_type = 0;

		// Buffered draw
		if( op.indexBuffer != nullptr )
		{
			index_type = (op.indexBuffer->getIndexSize() == IndexSize::INT32_INDEX) ? GL_UNSIGNED_INT : GL_UNSIGNED_SHORT;
			indices = reinterpret_cast<const GLvoid*>( (size_t)op.indexBuffer->getIndexSize() * op.vertexOffset );
			GLIndexBuffer::BindLock tmpLock( reinterpret_cast<GLuint>(op.indexBuffer->getHandle()) );
			glDrawElements(primitive, count, index_type, indices);
		}
		else
		{
			index_type = (op.unbufferedIndexSize == IndexSize::INT32_INDEX) ? GL_UNSIGNED_INT : GL_UNSIGNED_SHORT;
			size_t offset = reinterpret_cast<size_t>(op.unbufferedIndexData) + ((size_t)op.unbufferedIndexSize * op.vertexOffset);
			indices = reinterpret_cast<const GLvoid*>(offset);
			glDrawElements(primitive, count, index_type, indices);
		}

	}
	else
	{
		glDrawArrays(primitive, first, count);
	}

}

void GLGraphicsDevice::clearFrameBuffers(uint buffers, const Color& color, float depth, ushort stencil)
{
	GLbitfield flags = 0;
	if( buffers & FrameBufferType::COLOR )
	{
		flags |= GL_COLOR_BUFFER_BIT;
		//glColorMask(true, true, true, true);
		glClearColor(color.r, color.g, color.b, color.a);
	}
	if( buffers & FrameBufferType::DEPTH )
	{
		flags |= GL_DEPTH_BUFFER_BIT;
		glDepthMask(GL_TRUE);
		glClearDepth(depth);
	}
	if( buffers & FrameBufferType::STENCIL )
	{
		flags |= GL_STENCIL_BUFFER_BIT;
		// Enable buffer for writing if it isn't
		glStencilMask(0xFFFFFFFF);

		glClearStencil(stencil);
	}

	// Clear buffers
	glClear(flags);
}

NS_B2D_END